#pragma once

/*
提供实现日志输出到不同的文件中去
1 输出到标准输出文件中
2 输出到指定路径下的文件中
3 输出到滚动文件中去：存储的内容超过指定大小就新建一个存储日志文件
                    以天为单位管理日志文件
*/

/**
 实现方法
 1 提供一个抽象基类提供落地方法
 2 定义几个不同的派生类实现日志输出到不同的文件中去
 */

#include <iostream>
#include <filesystem>
#include <fstream>
#include <sstream>
#include <vector>
#include <unordered_map>
#include "Unit.hpp"
// 1 定义一个抽象基类
class LogLand
{
public:
    // 提供写入日志方法，按照字节数写入，一般不写入/0到文件中去
    virtual void WriteLog(const char *data, size_t len) = 0;
};

// 标准输出
class LogToStdOut : public LogLand
{
public:
    // 写入到标准输出,不能按照字符串的格式去输出，因为字符串是以\0作为结尾标识符的
    // 因为除了系统调用write函数是按字节输出的，其它的函数都是输出字符串都是以\0作为结尾标识符
    // 不会把/0及之后的字符输出到标准输出
    // 我们输出的日志中间也可能包含\0，所以要按字节输出
    void WriteLog(const char *data, size_t len) override
    {
        std::cout.write(data, len);
    }
};

// 输出到指定文件
// 标准输出
class LogToFile : public LogLand
{
public:
    LogToFile(const std::string &filepathname) : _filepathname(filepathname)
    {
        // 定义对象时就把文件打开,只要对象存在就一直打开文件
        // 不用每次都写入日志时在打开。
        // 1 创建日志所在的路径
        Unit::File::MakeDirector(Unit::File::GetPath(_filepathname));
        // 2 打开指定文件,以追加和二进制的方式打开文件，如果指定文件不存在则默认创建
        _outf.open(_filepathname, std::ios::binary | std::ios::app);
        if (!_outf.is_open()) // 判断是否打开成功
        {
            std::cerr << "Open failed" << "[" << __FILE__ << "]"
                      << "[" << __LINE__ << "]" << '\n';
        }
    }
    // 写入到指定文件
    void WriteLog(const char *data, size_t len) override
    {
        _outf.write(data, len);
        if (!_outf.good()) // 如果写入失败
        {
            std::cerr << "write failed" << "[" << __FILE__ << "]"
                      << "[" << __LINE__ << "]" << '\n';
        }
    }
    ~LogToFile()
    {
        _outf.close();
    }

private:
    std::string _filepathname; // 文件路径及名称
    std::ofstream _outf;       // 管理打开文件的流对象
};

// 滚动文件--以时间和文件大小来管理的文件
// 1 一个文件只能写入一个G大小的文件，超过了就新建文件写入
// 2 删除三天前的日志文件
class LogToRollFile : public LogLand
{
public:
    LogToRollFile(const std::string &basefilename, size_t max_fsize)
        : _filebase_pathname(basefilename), _max_fsize(max_fsize), _now_fsize(0), fnum(0)
    {
        // 定义对象时就把文件打开,只要对象存在就一直打开文件
        // 不用每次都写入日志时在打开。
        // 1 创建日志所在的路径
        std::string filepathname = CreateNewLogFilePath();

        std::cout << filepathname << std::endl;

        Unit::File::MakeDirector(Unit::File::GetPath(filepathname));
        std::shared_ptr<std::ofstream> outf = std::make_shared<std::ofstream>(); // 流对象不能直接复制，使用智能指针管理流对象所有权
                                                                                 // 2 创建文件
                                                                                 //  打开指定文件,以追加和二进制的方式打开文件

        outf->open(filepathname, std::ios::binary | std::ios::app);
        if (!outf->is_open()) // 判断是否打开成功
        {
            std::cerr << "Open failed" << "[" << __FILE__ << "]"
                      << "[" << __LINE__ << "]" << '\n';
        }
        _Openfss.insert({filepathname, outf});
        _IsoOpen.insert({filepathname, true});
        _CurOp = outf;
    }
    void WriteLog(const char *data, size_t len)override
    {
        if (_now_fsize > _max_fsize)
        {
            // 关闭之前打开的文件
            for (auto &kv : _IsoOpen)
            {
                if (kv.second == true)
                {
                    _Openfss[kv.first]->close();
                    kv.second == false;
                    break;
                }
            }
            // 创建新文件
            fnum++;
            std::string filepathname = CreateNewLogFilePath();
            std::cerr << filepathname << "[" << __FILE__ << "]"
                      << "[" << __LINE__ << "]" << '\n';
            std::shared_ptr<std::ofstream> outf = std::make_shared<std::ofstream>();
            outf->open(filepathname, std::ios::binary | std::ios::app);
            if (!outf->is_open()) // 判断是否打开成功
            {
                std::cerr << "Open failed" << "[" << __FILE__ << "]"
                          << "[" << __LINE__ << "]" << '\n';
            }
            _Openfss.insert({filepathname, outf});
            _IsoOpen.insert({filepathname, true});
            _CurOp = outf;
            _now_fsize = 0;
        }
        _CurOp->write(data, len);
        _now_fsize += len;
    }
    ~LogToRollFile()
    {
        for (auto &kv : _Openfss)
        {
            kv.second->close();
        }
    }

private:
    // 判断当前文件的大小并且创建新文件
    std::string CreateNewLogFilePath()
    {
        // 1 用时间构建文件名称
        struct tm timer;
        time_t tm = time(nullptr);
        localtime_r(&tm, &timer);
        std::stringstream ss;
        ss << timer.tm_year + 1900 << "-" << timer.tm_mon + 1
           << "-" << timer.tm_mday << "-" << timer.tm_hour << "-"
           << timer.tm_min << "-" << timer.tm_sec << "-" << fnum;
        std::string filename = _filebase_pathname + ss.str();
        return filename;
    }

private:
    // 通过基础文件名及路径加时间构成一个实际的输出文件名
    //  ./log/log1-  ->  ./log/log1-202211208012.log
    std::string _filebase_pathname;                                           // 文件路径及基础名称
    std::unordered_map<std::string, std::shared_ptr<std::ofstream>> _Openfss; // 以文件名管理打开文件的流对象集合
    std::unordered_map<std::string, bool> _IsoOpen;                           // 标记文件是否被打开
    size_t _max_fsize;                                                        // 每一个文件写入的最大大小                                                       // 输出到该文件的日志的最大大小
    std::shared_ptr<std::ofstream> _CurOp;                                    // 记录当前打开的文件
    size_t _now_fsize;                                                        // 当前打开的文件的大小
    uint32_t fnum;                                                            // 当前文件的编号
};

//定义一个工厂类来提供定义具体的日子落地对象方法
//由于每个落地对象的构造函数的参数和类型不一样，所以通过参数包
//模板类来控制
template<class LandType,class ...Args>
class LandFactory
{
public:
    //提供一个静态方法来创建一个落地对象的智能指针
    static std::shared_ptr<LogLand> CreateLogLand(Args&& ...args)
    {
        //使用完美转发把不同的落地对象的参数传递进去
        //将来用户提供什么类型和参数就返回什么类型的落地对象
        return std::make_shared<LandType>(std::forward<Args>(args)...);
    }
};
